Fase 4 CRISP-DM: Modelling A MEDIAS
Fase 5 CRISP-DM: Evaluation PENDIENTE
Fase 6 CRISP-DM: Deployment PENDIENTE
Para la realización de este trabajo voy a seguir la metodología estandar CRISP-DM.
Cross Industry Standard Process for Data Mining (CRISP-DM) es un proceso iterativo de creación de software, centrado en el análisis de datos, dividido en 6 fases:
Business Understanding
En esta fase se intenta clarificar el problema a resolver al igual que los objetivos y limitaciones de nuestra solución.
Data Understanding
En esta fase tenemos que obtener nuestros datos, explorarlos y verificar su calidad.
Data Preparation
En esta fase limpiaremos y formatearemos los datos para maximizar su potencial.
Modeling
En esta fase estudiaremos diferentes formas de realizar los modelos de recomendación y diseñare el modelo.
Evaluation
En esta fase estudiaremos el funcionamiento del modelo
Deployment
En esta fase se detallan los pasos de implementación del sistema en un ambiente profesional y las posibles mejoras.
El objetivo de este Trabajo de Fin de Grado es realizar un sistema de recomendación de películas basado en contenidos (Content-based recommender system), con verificación de recomendaciones basadas en las visualizaciones de otros usuarios.
Este trabajo no tiene un fin comercial. De todos modos, su objetivo comercial sería estudiar las técnicas de sistemas de recomendación y proporcionar una solucíon propia a este problema.
La restricción principal de este trabajo es el tiempo.
Debe ser presentado cualquiera de las siguientes tres convocatorias, Febrero, Junio o Septiembre.
El impacto de este trabajo será a nivel académico y de demostración de las tecnologías actuales de ML y de sistemas de codificación del lenguaje natural.
La pagina web de metacritic es un sitio web que recopila reseñas de álbumes de música, videojuegos, películas, programas de televisión y libros.
Para cada categoría de entretenimiento (musica, videojuegos, libros ...), Metacritic dispone de un top donde se encuentran todos los elementos organizados por nota.
En concreto, me voy a centrar en el top de películas (https://www.metacritic.com/browse/movies/score/metascore/all/filtered?page=0).
Este top está dispuesto (actualmente) en 145 páginas, cada uno con entradas correspondientes a 100 películas.
El conjunto de datos ha sido recogido por mi utilizando un programa de webscrapping realizado en python.
En concreto el programa de webscrapping sigue el siguiente esquema:
DiccionarioPeliculas=[]
Para cada numero x de 1 a 145:
pagina= descargarHTMLPagina(x);
Para cada numero y de 1 a 100:
pelicula = descargarHTMLPelicula(y)
datosPelicula=procesarPelicula(pelicula)
DiccionarioPeliculas.añadir(datosPelicula)
Para completar nuestra base de datos, nos interesaría mucho disponer de las reviews específicas de los usuarios para cada película que hemos recogido previamente.
Por lo tanto, usando otro programa de webscrapping realizaríamos la siguiente tarea:
PeliculasDeseadas = DiccionarioPeliculas
DiccionarioReviews=[]
Para cada pelicula en DiccionarioPeliculas:
HTML = descargarHTMLPelicula(pelicula)
RCs = procesarReviewsCriticos(HTML)
RUs = procesarReviewsUsers(HTML)
DiccionarioReviews.añadir(RCs)
DiccionarioReviews.añadir(RUs)
Primero importaré las librerías necesarias y los datasets de películas y reviews que hemos recopilado en el apartado anterior.
import pandas as pd #Librería pandas para el uso de dataframes
import numpy as np #Libreria Numpy para el uso de operaciones como select
#import seaborn as sns # Librería Seaborn para
import plotly.express as px #Plotly express para gráficos simples
import plotly.graph_objects as go #Plotly graphobjects para modificar gráficos
from nltk.corpus import words # Natural language toolkit para detectar si las reviews están en inglés
import nltk
# Comment this if the data visualisations doesn't work on your side
%matplotlib inline
#Import de los datasets de mi directorio local
dfMovies = pd.read_csv('moviedataset3_11_21.csv')
dfReviews = pd.read_csv('reviewDataSet.csv')#,error_bad_lines=False, engine="python"
#Descarga del conjunto de datos de palabras en inglés
nltk.download('words')
[nltk_data] Downloading package words to /home/pablo/nltk_data... [nltk_data] Package words is already up-to-date!
True
Habiendo importado los datasets a un dataframe de la librería pandas correctamente, usaré el info para mostrar el número de entradas y los atributos de cada dataset.
dfMovies.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14213 entries, 0 to 14212 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 title 14213 non-null object 1 age_rating 14213 non-null object 2 rating 14213 non-null object 3 rank 14213 non-null int64 4 genre 14213 non-null object 5 director 14213 non-null object 6 year 14213 non-null int64 7 producer 13941 non-null object 8 actor 14213 non-null object 9 runtime 14213 non-null int64 10 description 14210 non-null object 11 img 14213 non-null object 12 url 14213 non-null object dtypes: int64(3), object(10) memory usage: 1.4+ MB
Como podemos ver, disponemos de 13255 entradas con 12 columnas.
Solo 4 de estas columnas son variables discretas, rating, year of release, runtime y rank.
Rating contiene la nota de 0 a 10 impuesta por los usuarios.
Rank contiene la posición 1-15000 en la que esta película (impuesto por metacritic)
Year contiene el año de salida de la película.
Runtime contiene la duración de la película.
Es esperable que los atributos rating y rank esten fuertemente correlados ya que expresan información muy parecida. Esta afirmación la comprobaré mas tarde, ya que si el valor de correlación es muy alto podríamos considerar un solo atributo.
Por otro lado las variables categóricas son las siguientes:
Title: contiene el titulo de la película.
age_rating: indica la edad mínima recomendada para la visualización. Posteriormente haré un análisis para representarla como un entero.
genre: indica el género.
director: nombre del director principal de la película.
producer: nombre de la productora.
actor: nombre del actor principal de la película.
description: parrafo con la descripción de la película.
img: url de la carátula de la película.
url: url de la página de metacritic donde se describe la película.
Por otro lado, la composición de nuestro dataset de reviews es la siguiente:
dfReviews.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 584150 entries, 0 to 584149 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 title 584150 non-null object 1 user 584148 non-null object 2 type 584150 non-null object 3 grade 584150 non-null int64 4 review 584150 non-null object dtypes: int64(1), object(4) memory usage: 22.3+ MB
Como podemos ver tiene 5 atributos.
Title: string que contiene el nombre de la película que se está puntuando.
User: string que contiene el nombre de usuario del autor de la review.
Type: variable que identifica si el autor es un "reviewer profesional" o un usuario de la web.
Grade: nota de la review.
Review: string que contiene la valoración de la película.
fig = px.pie(dfMovies, names=dfMovies["genre"].value_counts().index, values=dfMovies["genre"].value_counts())
fig.update_traces(hoverinfo='value', textinfo='label+percent')
fig.update_layout(autosize=False,width=1000,height=1000,)
fig.show();
Como podemos observar, nuestro dataset está compuesto por películas dispuestas en 22 géneros.
El drama es el género más común por grán mayoría. Seguido por acción Acción, documentales y biografías.
Como no vamos a realizar una predicción de genéneros de películas, en principio, este desbalanceo no presentará un problema en el futuro.
Por otra parte, ratings de edad de las películas se disponen de la siguiente forma:
fig = px.bar(dfMovies, x=dfMovies["age_rating"].value_counts().index, y=dfMovies["age_rating"].value_counts())
fig.update_layout(autosize=False,width=1000,height=300,)
fig.show();
Los años de salida siguen la siguiente distribución
fig = px.histogram(dfMovies, x=dfMovies["year"].value_counts().index, y=dfMovies["year"].value_counts(),nbins=130)
fig.update_layout(autosize=False,width=1000,height=300,)
fig.show();
Como podemos observar, desde el año 2000 a habido un crecimiento lineal en la producción de películas, a excepción de 2 parones en los siguientes años:
prodCounts=pd.DataFrame(dfMovies["producer"].value_counts()).head(30)
test_tree = go.Figure(go.Treemap(
labels = prodCounts["producer"].index,
parents=[""]*len(prodCounts["producer"].index),
values = prodCounts["producer"].values,
textinfo = "label+value"
))
test_tree.show();
Para mostrar el tiempo de ejecución voy a usar un diagrama de "caja y bigotes" o boxplot.
El resultado es el siguiente:
fig = px.box(dfMovies, x="runtime", title="Boxplot representativo del tiempo de ejecución.")
fig.show();
Como podemos observar, la mediana en cuanto a duración es 101 minutos.
Los valores extremos se situan en 62 y 144 minutos. Las duraciones fuera de este rango se consideran datos atípicos o outliers.
También vamos a visualizar los 25 actores principales más comunes.
actorCounts=pd.DataFrame(dfMovies["actor"].value_counts())[1:26]
fig = px.pie(actorCounts, names=actorCounts["actor"].index, values=actorCounts["actor"])
fig.update_layout(autosize=False,width=900,height=490,)
fig.update_traces(hoverinfo='value', textinfo='label+percent')
fig.show();
directorCounts=pd.DataFrame(dfMovies["director"].value_counts()).head(25)
z=list(range(1, 26))
fig = go.Figure(data=[go.Bar(
x=directorCounts["director"].index,
y=directorCounts["director"],
marker=dict(color = z, colorscale='Portland'))
])
fig.update_layout(title_text='Top 25 actores');
Con respecto a las notas de las reviews, voy a realizar un boxplot para representar las notas de los usuarios y los reviewers.
fig = px.box(dfReviews, x="grade",y="type")
fig.show()
Como podemos observar, las notas de los reviewers se encuentran en el rango de 0 a 100 y las de los usuarios, de 0 a 10.
Por lo tanto, esto lo tendremos que tener en cuenta en el apartado de data preparation.
reviewcounts=pd.DataFrame(dfReviews["title"].value_counts()).head(40)
test_tree = go.Figure(go.Treemap(
labels = reviewcounts["title"].index,
parents=[""]*len(reviewcounts["title"].index),
values = reviewcounts["title"].values,
textinfo = "label+value"
))
test_tree.show();
Para verificar la calidad de nuestros conjuntos de datos verificaré los siguientes criterios:
Para comprobar la unicidad de nuestros conjuntos de datos podemos utilizar el siguiente método de la librería pandas
Pandas.DataFrame.duplicated()
Este método, dado un dataframe, devuelve, para cada una de las entradas, si es única o no.
Agrupando este resultado con la función value_counts() obtenemos el siguiente resultado.
print("Reviews duplicadas")
print(dfReviews.duplicated().value_counts())
print("")
print("Peliculas duplicadas")
print(dfMovies.duplicated().value_counts())
Reviews duplicadas False 583727 True 423 dtype: int64 Peliculas duplicadas False 14213 dtype: int64
Como podemos observar, existen 423 reviews duplicadas y 0 películas duplicadas.
Por lo tanto, podemos ejecutar la función drop_duplicates() de los dataframes de la librería pandas y eliminar estos valores repetidos.
Si comprobasemos ahora los resultados de la función anterior obtendriamos 0 repetidos.
dfReviews=dfReviews.drop_duplicates()
dfReviews.duplicated().value_counts()
False 583727 dtype: int64
Considero que tanto el dataset de reviews como el de películas son muy completos en cuanto a contenido y numero de entradas.
Ambos datasets contienen todas las entradas en cuanto a películas o críticas disponibles en metacritic.
No existe ninguna dataset publica de Metacritic para la sección de películas. Por lo tanto compararé mi dataset con los 3 datasets de reviews de películas mas "relevantes" según el buscador de Kaggle:
El conjunto de datos de Películas es válido ya que ha sido creado por empleados de metacritic y solo contiene datos factuales.
Por otro lado, algunas atributos son nulos o están por determinar si la película no ha salido todavía al público.
El conjunto de datos de reviews si que puede presentar problemas de validez debido a que las reviews de los usuarios no son filtradas.
Durante el estudio exploratorio del conjunto de datos he encontrado las siguientes inconsistencias:
Las clasificaciones de edad de cada película usan las convenciones particulares de cuando esa película fue grabada.
Por lo tanto, disponemos de 24 labels categóricas que podrían ser remplazadas por unos pocos números enteros.
Las reviews de los criticos estan sobre 100 en metacritic y las de los users sobre 10.
Algunas reviews incluyen la string "This review contains spoilers, click expand to view." indicando que esa review tiene spoilers. Este patrón será eliminado del contenido de la review y indicado en una nueva columna de nuestro dataset.
Como hemos visto en el análisis exploratorio, las reviews son mayoritariamente en inglés. De todos modos, existen algunas reviews que su contenido de palabras en inglés es muy bajo, por lo tanto, están en otro idioma (o repletas de faltas de ortografía).
El dataset de películas es preciso ya que ha sido realizado por profesionales encargados de gestionar la página de metacritic.
El dataset de reviews tiene un contendio subjetivo, por lo tanto no podemos medir su precisión. De todos modos cabe destacar que las reviews esta categorizadas por si son de Reviewers o de Usuarios.
Las reviews de reviewers tendrán un contenido por lo general representativo y bien escrito.
Las reviews de usuarios no están filtradas, por lo tanto pueden ser tanto muy representativas, como totalmente arbitrarias.
Como hemos podido observar en el gráfico anterior, esta columna está muy desbalanceada, a demas tiene un alto contenido de valores nulos. De todos modos, vamos a realizar data wrangling de forma que agrupamos estas variables categóricas en edades como numeros enteros.
print("Edades antes de procesado",dfMovies["age_rating"].unique() )
df_mod=dfMovies.replace(" | Approved", 15)
df_mod=df_mod.replace(" | R", 18)
df_mod=df_mod.replace(' | TV-G', 0)
df_mod=df_mod.replace(' | TV-PG', 15)
df_mod=df_mod.replace(' | PG', 15)
df_mod=df_mod.replace(' | G', 0)
df_mod=df_mod.replace(' | Passed', 0)
df_mod=df_mod.replace(' | Not Rated', -5)
df_mod=df_mod.replace(' | PG-13', 13)
df_mod=df_mod.replace(' | PG-13`', 13)
df_mod=df_mod.replace(' | GP', 0)
df_mod=df_mod.replace(' | M', 15)
df_mod=df_mod.replace(' | M/PG', 15)
df_mod=df_mod.replace(' | TV-MA', 17)
df_mod=df_mod.replace(' | Unrated', -5)
df_mod=df_mod.replace('Not Rated', -5)
df_mod=df_mod.replace(' | TV-14', 14)
df_mod=df_mod.replace(' | NC-17', 17)
df_mod=df_mod.replace(' | NR', -5)
df_mod=df_mod.replace(' | Open', 14)
df_mod=df_mod.replace(' | X', 18)
df_mod=df_mod.replace(' | MA-17', 14)
df_mod=df_mod.replace(' | PG--13', 13)
df_mod=df_mod.replace(' | TV-Y7-FV', 7)
df_mod=df_mod.replace(' | TV-Y7', 7)
print()
print()
print("Edades despues de procesado",df_mod["age_rating"].unique() )
Edades antes de procesado [' | Approved' ' | R' ' | TV-G' ' | TV-PG' ' | PG' ' | G' ' | Passed' ' | Not Rated' ' | PG-13' ' | GP' ' | M' ' | M/PG' ' | TV-MA' ' | Unrated' 'Not Rated' ' | TV-14' ' | NC-17' ' | NR' ' | Open' ' | X' ' | MA-17' ' | PG--13' ' | PG-13`' ' | TV-Y7-FV' ' | TV-Y7'] Edades despues de procesado [15 18 0 -5 13 17 14 7]
fig = px.bar(df_mod, x=df_mod["age_rating"].value_counts().index, y=df_mod["age_rating"].value_counts())
fig.update_layout(autosize=False,width=700,height=500,)
fig.show();
Como podemos observar, hemos eliminado las variables categóricas y el resultado es mucho mas claro.
Seguimos teniendo una gran cantidad de valores sin calificar (valor -1 en el eje x), sin embargo, como la gran mayoría de valores se encuentran entre 13 y 18 creo que sería buena idea sustituirlo por el valor medio.
print(df_mod['age_rating'].loc[df_mod['age_rating'] > -1].value_counts())
print()
print("El valor medio es ",df_mod['age_rating'].loc[df_mod['age_rating'] > 0].mean(axis=0)," por lo tanto susutituiré las entradas no calificadas por 16")
18 4693 13 2442 15 1654 0 348 17 312 14 146 7 2 Name: age_rating, dtype: int64 El valor medio es 16.044112877067793 por lo tanto susutituiré las entradas no calificadas por 16
df_mod=df_mod.replace(-5, 16)
Esta distribución sería más apropiada teniendo en cuenta los datos no nulos.
Como hemos visto en el apartado 2.3 Exploring data, las calificaciones realizadas por los usuarios se encuentran en el rango de 0 a 10 y las de los críticos en el rango de 0 a 100.
Por lo tanto, vamos a multiplicar las calificaciones de los usuarios por 10 para que todas las calificaciones se encuentren en el mismo intervalo.
def rescaler(x,y):
if y == "user":
return x*10
else:
return x
dfReviews['grade'] = dfReviews.apply(lambda x: rescaler(x.grade, x.type), axis=1)
Tras aplicar una función lambda, podemos comprobar el resultado con el mismo boxplot que antes.
fig = px.box(dfReviews, x="grade",y="type")
fig.show()
Muchas de las reviews de nuestro dataset contienen spoilers.
Metacritic incluye la siguiente subcadena en las reviews con spoilers.
"This review contains spoilers, click expand to view."
print("Reviews que contienen la substring:'This review contains spoilers, click expand to view.' ")
print(dfReviews['review'].str.contains("This review contains spoilers, click expand to view.").value_counts())
Reviews que contienen la substring:'This review contains spoilers, click expand to view.' False 567333 True 16394 Name: review, dtype: int64
Eliminaremos esta subcadena de nuestras reviews para eliminar un posible efecto negativo en el algoritmo de vectorización.
De todos modos, añadiremos una nueva columna a nuestro dataset que indique si la review contiene spoilers o no.
conditions = list(map(dfReviews['review'].str.contains, ["This review contains spoilers, click expand to view."]))
dfReviews['spoilers'] =np.select(conditions, ["This review contains spoilers, click expand to view."], 0)
dfReviews.loc[dfReviews['review'].str.contains('This review contains spoilers, click expand to view.'), 'spoilers'] = 1
dfReviews['review']=dfReviews['review'].str.replace("This review contains spoilers, click expand to view.", "")
Para comprobar si el filtrado ha funcionado correctamente volveremos a ejecutar el método contains agrupado por la función value_counts()
print("Reviews que contienen la substring:'This review contains spoilers, click expand to view.' Despues de filtrado")
print(dfReviews['review'].str.contains("This review contains spoilers, click expand to view.").value_counts())
Reviews que contienen la substring:'This review contains spoilers, click expand to view.' Despues de filtrado False 583727 Name: review, dtype: int64
Como hemos comprobado previamente, el contenido de las reviews se encuentra escrito mayoritariamente en inglés.
El metodo programado anteriormente nos indicaba que porcentaje de la review está en inglés.
Para no tener que designar 'a ojo' un límite que separe el contenido en inglés del de otros idiomas vamos a delegar esta tarea en diferentes librerías de procesado de lenguaje capaces de reconocer el idioma de un texto.
La metodología que voy a seguir es:
Si la mayoría de ellas (2 de 3 librerías) detectan el mismo idioma, entonces, clasificaríamos el idioma de la review como el idioma coincidente.
%%capture
!pip install langid
!pip install pycld3
!pip install fasttext
import re
import langid
import cld3
import fasttext
model = fasttext.load_model('lid.176.ftz')
dfReviews['review']=dfReviews['review'].str.replace("\n", "")
Ahora ejecutaríamos el siguiente código para categorizar el lenguaje de las reviews.
%%script false --no-raise-error
reviewLangDF = {'cld3': [], 'fasttext': [], 'langid': []}
for index, row in dfReviews.drop_duplicates().iterrows():
if cld3.get_language(row['review']) is not None:
reviewLangDF['cld3'].append(cld3.get_language(row['review']).language)
else:
reviewLangDF['cld3'].append("unknown")
reviewLangDF['fasttext'].append(model.predict(row['review'], k=1)[0][0].replace('__label__', ''))
reviewLangDF['langid'].append(langid.classify(row['review'])[0])
reviewLangDF=pd.DataFrame(reviewLangDF)
reviewLangDF.to_csv("languages_processed.csv", index=False)
Este bucle tarda alrededor de una hora en finalizar, por lo tanto usando la indicación
%%script false --no-raise-error
indico que esa celda de código no va a ser ejecutada.
En una ejecución previa, guardé su contenido como CSV y desde ese momento, leo un fichero con su contenido.
dfLangProc = pd.read_csv('languages_processed.csv')
indeciso=0
reviewLang = {'language': []}
for index, row in dfLangProc.iterrows():
if row['langid']==row['cld3']==row['fasttext']:
reviewLang['language'].append(row['langid'])
elif row['langid']==row['cld3']:
reviewLang['language'].append(row['langid'])
elif row['fasttext']==row['cld3']:
reviewLang['language'].append(row['fasttext'])
elif row['langid']==row['fasttext']:
reviewLang['language'].append(row['langid'])
else:
reviewLang['language'].append("unknown")
langdf=pd.DataFrame(reviewLang)
dfReviews=dfReviews.join(langdf)
Despues de añadir el idioma a nuestro dataframe podemos ver como se distribuyen las reviews.
dfReviews['language'].value_counts().plot(kind='bar', stacked=True, log=True, figsize=(20,6), title="Idioma de las reviews (Escala Logarítmica)")
pd.DataFrame(dfReviews['language'].value_counts()).head()
| language | |
|---|---|
| en | 565765 |
| es | 4925 |
| unknown | 4696 |
| pt | 3148 |
| fr | 1407 |
#dfReviews= dfReviews.applymap(lambda s:s.lower() if type(s) == str else s)
#df_mod= df_mod.applymap(lambda s:s.lower() if type(s) == str else s)
#dfReviews=dfReviews.replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True)
#df_mod=df_mod.replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True)
Stopwords son conectores que carecen de valor gramatical (the, and, to,that ...)
Generalmente durante el entrenamiento de los embedders esto se han tenido en cuenta. Como indica el paper(https://proceedings.neurips.cc/paper/2013/file/9aa42b31882ec039965f3c4923ce901b-Paper.pdf):
We show that subsampling of frequent words during training results in a significant speedup (around 2x - 10x), and improves accuracy of the representations of less frequent words.
En caso que quisiesemos eliminarlas podríamos restar a nuestras review la intersección de la review con el siguiente conjunto proporcionado por NLTK (nltk.download('stopwords')):
set(stopwords.words("english"))
Tras haber limpiado nuestros datasets podemos exportarlos en nuevos ficheros.
dfReviews.to_csv('reviewDatasetClean.csv',index=False)
df_mod.to_csv('movieDatasetClean.csv',index=False)
En este apartado nos vamos a centrar en explicar que es un embedding, las diferentes técnicas de generación de embeddings y como pensamos utilizarlo en el recomendador de películas.
Los modelos de machine learning, por lo general, utilizan vectores como parametros de entrada.
Por lo tanto, cuando trabajamos con palabras o texto, lo primero que necesitamos hacer es convertir estas strings a vectores.
Una de las grandes ventajas que presentan las técnicas de embeddings, es que el siginificado de las palabras se ve reflejado en la diferentes carácterísticas de los vectores.
La red neuronal word2vec fue una de las primeras victorias en el campo del NLP con redes neuronales.
Word2vec es capaz de, dado una palabra, retornar un vector que codifica esa palabra.
La copa oculta tiene N neuronas.
La capa de salida vuelve a ser un vector one hot de dimension V con una distriubcion de probabilidad realizada usando softmax.
La capa de entrada es un vector one hot encoded de longitud V.
La capa oculta contiene N neuronas.
La capa de salida contiene C distribuciones de probabilidad con V probilidades cada una (una para cada palabra)(equivalente al de entrada) cuyos valores son los valores porcesados por softmax.
En ambos casos las redes neuronales se entrenan usando softmax.
Por último, uno de los problemas que nos presenta Word2Vec es que ha sido diseñado para procesar embeddings palabra por palabra. Por lo tanto, para realizar el embedding de un texto, lo que haré será realizar la media de todos los vectores que lo componen.
Why not finetunning bert: finetunning for labeled data
Provisionalemente: Como sistema de recomendación, el planteamiento a seguir es buscar las películas tales que la similitud del coseno de los diferentes vectores es máxima.
Idealmente: hacer one hot del genero, añadir los valores numéricos como recomendación y nota y vectores de embedding y pasarlos a knn, solicitar al modelo los K más cercanos.</font>
Para realizar nuestro modelo basado en Word2Vec voy a seguir el siguiente proceso:
Primero importamos las librerías necesarias.
from gensim.models import KeyedVectors #Importar modelo desde fichero
from gensim.models import Word2Vec #Importar word2vec
from datetime import datetime #tracking de tiempo en realizar de embeddings
from scipy import spatial #similitud del coseno
from operator import itemgetter # comparación de tuplas
Cargamos nuestro dataset de películas, y por si acaso (no debería haber ninguna), eliminamos las películas con descripciones nulas.
df = pd.read_csv("movieDatasetClean.csv")
df= df[df['description'].notnull()]
Realizamos un corpus de palabras que contenga todas las reviews de nuestro dataset separadas en palabras.
También obtenemos el modelo básico de word2vec "GoogleNews-vectors-negative300". Este modelo fue entrenado por Google usando un dataset de Google News con alrededor de 100 mil millones de palabras.
Este modelo contiene vectores 300-dimensonales para 3 millones de palabras.
%%script false --no-raise-error
corpus = []
for words in df['description']:
corpus.append(words.split())
print("Comienzo: ",datetime.now())
model = KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin.gz', binary=True)
print("Cargado: ",datetime.now())
Como hemos explicado antes, word2vec devuelve un vector dado una palabra.
Por lo tanto, para interpretar las decripciones de las películas podemos realizar la media entre todos los vectores "palabra" de cada descripción y ver si los resultados son prometedores.
def get_mean_vector(model, words):
words = [word for word in words if word in list(model.index_to_key)]
if len(words) >= 1:
return np.mean(model[words], axis=0)
else:
return []
Esta es la celda de código más pesada, donde obtendríamos los vectores medios de cada descripción.
%%script false --no-raise-error
i=0
vectors=[]
for doc in corpus:
vec = get_mean_vector(model, doc)
if len(vec) > 0:
vectors.append(vec)
if i%500==0:
print(i,datetime.now())
i=i+1
else:
vectors.append([])
vectors_full=pd.DataFrame(vectors)
vectors_movie=df.join(vectors_full)
Llegados a este punto podemos importar los vectores que habíamos calculado en una ejecución anterior y empezar a ver los resultados.
movies_word2vec=pd.read_csv("movies_word2vec.csv")
vectors_movie=movies_word2vec.iloc[: , 14:314]
Como medida de similitud entre descripciones de películas (vectores), voy a usar la distancia del coseno.
La distancia del coseno se suele utilizar como media de similitud en procesamiento de texto.
La similitud del coseno tiene en cuenta la orientación de los vectores y no la magnitud.
def get_film_similarity(id1,id2):
return 1 - spatial.distance.cosine(vectors_movie.iloc[id1],vectors_movie.iloc[id2] )
Habiendo descrito nuestra medida de distancia podemos, dada una película, buscar las películas que tienen descripciones más parecidas.
def KNNrecommender(id_movie,n,verbose):
toplist=[]
for i in range (0,14209):
if (i==id_movie):
idOriginal=movies_word2vec.iloc[id_movie]["rank"]
else:
similarity=get_film_similarity(i,id_movie)
toplist.append(tuple((similarity, i)))
toplist.sort(key=itemgetter(0),reverse=True)
if verbose:
print("Dada la pelicula ",movies_word2vec.iloc[id_movie]["title"], "recomiendo:")
print()
i=1
for peli in toplist[0:n]:
if verbose:
print(str(i)+")",movies_word2vec.iloc[peli[1]]["title"])
print("(genero:",movies_word2vec.iloc[peli[1]]["genre"],"nota:",movies_word2vec.iloc[peli[1]]["rating"],")")
print("Parecido del ", int(peli[0]*100),"%")
print()
print()
i=i+1
return toplist[0:n]
Visto todo el código, vamos a comprobar las recomendaciones de varias películas
recomendacionesElPadrino=KNNrecommender(id_movie=1,n=10,verbose=True) #dado el padrino
Dada la pelicula The Godfather recomiendo: 1) The Godfather: Part III (genero: Action nota: 7.7 ) Parecido del 85 % 2) This So-Called Disaster: Sam Shepard Directs the Late Henry Moss (genero: Documentary nota: tbd ) Parecido del 85 % 3) The Godfather: Part II (genero: Drama nota: 9.0 ) Parecido del 84 % 4) Hello Lonesome (genero: Drama nota: tbd ) Parecido del 84 % 5) Gonzo: The Life and Work of Dr. Hunter S. Thompson (genero: Biography nota: 7.7 ) Parecido del 83 % 6) Romeo + Juliet (genero: Drama nota: 6.4 ) Parecido del 83 % 7) Three Colors: Red (genero: Drama nota: 8.6 ) Parecido del 82 % 8) The Godfather Coda: The Death of Michael Corleone (genero: Action nota: 7.5 ) Parecido del 82 % 9) Howl (genero: Drama nota: 6.7 ) Parecido del 82 % 10) Off the Hook (genero: untagged nota: tbd ) Parecido del 82 %
recomendacionesAvengers=KNNrecommender(id_movie=1878,n=5,verbose=True)
Dada la pelicula Avengers: Endgame recomiendo: 1) Captain Marvel (genero: Action nota: 2.9 ) Parecido del 80 % 2) Avengers: Age of Ultron (genero: Action nota: 7.0 ) Parecido del 79 % 3) Avengers: Infinity War (genero: Action nota: 8.6 ) Parecido del 79 % 4) Harry Potter and the Deathly Hallows: Part I (genero: Action nota: 7.6 ) Parecido del 78 % 5) Captain America: Civil War (genero: Action nota: 8.2 ) Parecido del 78 %
recomendacionesHarryPotter=KNNrecommender(id_movie=630,n=5,verbose=True)
Dada la pelicula Harry Potter and the Deathly Hallows: Part 2 recomiendo: 1) Hellboy II: The Golden Army (genero: Action nota: 7.4 ) Parecido del 87 % 2) Aquaman (genero: Action nota: 6.9 ) Parecido del 86 % 3) Avengers: Age of Ultron (genero: Action nota: 7.0 ) Parecido del 86 % 4) The Hudsucker Proxy (genero: Drama nota: 7.2 ) Parecido del 86 % 5) Blizzard of Souls (genero: Drama nota: tbd ) Parecido del 86 %
def getTitle(id1):
return (df_mod["title"].loc[df_mod['rank'] == id1].values.item())
def validateRecommendation(id1,id2):
title1=getTitle(id1)
title2=getTitle(id2)
userReviews1=dfReviews["user"].loc[(dfReviews['title'] == title1) & (dfReviews['grade']>50)]
userReviews2=dfReviews["user"].loc[(dfReviews['title'] == title2) & (dfReviews['grade']>50)]
#print("Recomendación positivamente validada por", validateRecomendation(movies_word2vec.iloc[peli[1]]["rank"],idOriginal), "reviewers.")
intersected_df = pd.merge(userReviews1, userReviews2, how='inner')
return len(intersected_df)
def validateList(idMovie,recommendedList,verbose):
i =1
validations=[]
for recomendation in recommendedList:
valoracionesPositivas=validateRecommendation(idMovie,recomendation[1])
validations.append(tuple(( recomendation[1],valoracionesPositivas)))
if verbose:
print(str(i)+")","'"+movies_word2vec.iloc[recomendation[1]]["title"]+"'.")
print("Recomendación automática validada positivamente por ",valoracionesPositivas ,"reviewers.")
print()
i=i+1
return validations
#print("Recomendación positivamente validada por", validateRecomendation(movies_word2vec.iloc[peli[1]]["rank"],idOriginal), "reviewers.")
validacionesElPadrino=validateList(idMovie=1,recommendedList=recomendacionesElPadrino,verbose=True)
1) 'The Godfather: Part III'. Recomendación automática validada positivamente por 2 reviewers. 2) 'This So-Called Disaster: Sam Shepard Directs the Late Henry Moss'. Recomendación automática validada positivamente por 7 reviewers. 3) 'The Godfather: Part II'. Recomendación automática validada positivamente por 11 reviewers. 4) 'Hello Lonesome'. Recomendación automática validada positivamente por 4 reviewers. 5) 'Gonzo: The Life and Work of Dr. Hunter S. Thompson'. Recomendación automática validada positivamente por 5 reviewers. 6) 'Romeo + Juliet'. Recomendación automática validada positivamente por 3 reviewers. 7) 'Three Colors: Red'. Recomendación automática validada positivamente por 19 reviewers. 8) 'The Godfather Coda: The Death of Michael Corleone'. Recomendación automática validada positivamente por 19 reviewers. 9) 'Howl'. Recomendación automática validada positivamente por 5 reviewers. 10) 'Off the Hook'. Recomendación automática validada positivamente por 4 reviewers.
validacionesHP=validateList(idMovie=630,recommendedList=recomendacionesHarryPotter,verbose=True)
1) 'Hellboy II: The Golden Army'. Recomendación automática validada positivamente por 5 reviewers. 2) 'Aquaman'. Recomendación automática validada positivamente por 2 reviewers. 3) 'Avengers: Age of Ultron'. Recomendación automática validada positivamente por 17 reviewers. 4) 'The Hudsucker Proxy'. Recomendación automática validada positivamente por 3 reviewers. 5) 'Blizzard of Souls'. Recomendación automática validada positivamente por 6 reviewers.
validacionesAvengers=validateList(idMovie=1878,recommendedList=recomendacionesAvengers,verbose=True)
1) 'Captain Marvel'. Recomendación automática validada positivamente por 11 reviewers. 2) 'Avengers: Age of Ultron'. Recomendación automática validada positivamente por 19 reviewers. 3) 'Avengers: Infinity War'. Recomendación automática validada positivamente por 8 reviewers. 4) 'Harry Potter and the Deathly Hallows: Part I'. Recomendación automática validada positivamente por 9 reviewers. 5) 'Captain America: Civil War'. Recomendación automática validada positivamente por 3 reviewers.
PENDIENTE: ¿pasar de notebook a py y hacer gui?
CRISP DM Pete Chapman, Julian Clinton, Randy Kerber, Thomas Khabaza, Thomas Reinartz, Colin Shearer, and Rüdiger Wirth (2000); The CRISP-DM User Guide https://www.the-modeling-agency.com/crisp-dm.pdf
Citar los 3 datasets
Metacritic top
Beautiful soup documentation
La web donde explican lo de STEM,TOR, proxy... </font>
https://stackoverflow.com/questions/39142778/python-how-to-determine-the-language
Retrain bert https://www.kaggle.com/abhishek/bertsrc